Backport path_find_first_component()
authorArnaud Rebillout <arnaudr@debian.org>
Tue, 7 Apr 2026 01:03:53 +0000 (08:03 +0700)
committerArnaud Rebillout <arnaudr@debian.org>
Mon, 13 Apr 2026 07:18:40 +0000 (14:18 +0700)
This is a prerequisite to backport path_startswith_full().

path_find_first_component() was introduced in systemd v249, in commit
0ee54dd4e2f: "path-util: introduce path_find_first_component()".

The function didn't change since it was introduced: it's still the same code in
systemd v260. However the test associated with it was slightly adjusted
afterward.

This commit backports the fonction and associated unit tests from systemd v260
(ie. with the latest version of the unit tests).

Forwarded: not-needed

Gbp-Pq: Name CVE-2026-29111-1.patch

src/basic/path-util.c
src/basic/path-util.h
src/test/test-path-util.c

index 794599a172b5ed788ea75ddaffe29aca621d67c8..d6e66970fe3ccf91d83c6cabd9c9ce6467c58590 100644 (file)
@@ -729,6 +729,90 @@ int fsck_exists(const char *fstype) {
         return executable_is_good(checker);
 }
 
+static const char* skip_slash_or_dot(const char *p) {
+        for (; !isempty(p); p++) {
+                if (*p == '/')
+                        continue;
+                if (startswith(p, "./")) {
+                        p++;
+                        continue;
+                }
+                break;
+        }
+        return p;
+}
+
+int path_find_first_component(const char **p, bool accept_dot_dot, const char **ret) {
+        const char *q, *first, *end_first, *next;
+        size_t len;
+
+        assert(p);
+
+        /* When a path is input, then returns the pointer to the first component and its length, and
+         * move the input pointer to the next component or nul. This skips both over any '/'
+         * immediately *before* and *after* the first component before returning.
+         *
+         * Examples
+         *   Input:  p: "//.//aaa///bbbbb/cc"
+         *   Output: p: "bbbbb///cc"
+         *           ret: "aaa///bbbbb/cc"
+         *           return value: 3 (== strlen("aaa"))
+         *
+         *   Input:  p: "aaa//"
+         *   Output: p: (pointer to NUL)
+         *           ret: "aaa//"
+         *           return value: 3 (== strlen("aaa"))
+         *
+         *   Input:  p: "/", ".", ""
+         *   Output: p: (pointer to NUL)
+         *           ret: NULL
+         *           return value: 0
+         *
+         *   Input:  p: NULL
+         *   Output: p: NULL
+         *           ret: NULL
+         *           return value: 0
+         *
+         *   Input:  p: "(too long component)"
+         *   Output: return value: -EINVAL
+         *
+         *   (when accept_dot_dot is false)
+         *   Input:  p: "//..//aaa///bbbbb/cc"
+         *   Output: return value: -EINVAL
+         */
+
+        q = *p;
+
+        first = skip_slash_or_dot(q);
+        if (isempty(first)) {
+                *p = first;
+                if (ret)
+                        *ret = NULL;
+                return 0;
+        }
+        if (streq(first, ".")) {
+                *p = first + 1;
+                if (ret)
+                        *ret = NULL;
+                return 0;
+        }
+
+        end_first = strchrnul(first, '/');
+        len = end_first - first;
+
+        if (len > NAME_MAX)
+                return -EINVAL;
+        if (!accept_dot_dot && len == 2 && first[0] == '.' && first[1] == '.')
+                return -EINVAL;
+
+        next = skip_slash_or_dot(end_first);
+
+        *p = next + streq(next, ".");
+        if (ret)
+                *ret = first;
+        return len;
+}
+
 int parse_path_argument_and_warn(const char *path, bool suppress_root, char **arg) {
         char *p;
         int r;
index d613709f0b024b0866f90be32e9577ce5399e974..af82624e489c397afebd441c1b24f6d62043f35d 100644 (file)
@@ -147,6 +147,7 @@ int fsck_exists(const char *fstype);
 int parse_path_argument_and_warn(const char *path, bool suppress_root, char **arg);
 
 char* dirname_malloc(const char *path);
+int path_find_first_component(const char **p, bool accept_dot_dot, const char **ret);
 const char *last_path_component(const char *path);
 int path_extract_filename(const char *p, char **ret);
 
index cb91a1a979d4a5381ef1a5a29a49adbbd5f54778..699aacefd459826dac38e20c9b87a3c1b0143420 100644 (file)
@@ -503,6 +503,98 @@ static void test_file_in_same_dir(void) {
         free(t);
 }
 
+static void test_path_find_first_component_one(
+                const char *path,
+                bool accept_dot_dot,
+                char **expected,
+                int ret) {
+
+        log_debug("/* %s(\"%s\", accept_dot_dot=%s) */", __func__, strnull(path), yes_no(accept_dot_dot));
+
+        for (const char *p = path;;) {
+                const char *e;
+                int r;
+
+                r = path_find_first_component(&p, accept_dot_dot, &e);
+                if (r <= 0) {
+                        if (r == 0) {
+                                if (path) {
+                                        assert_se(p == path + strlen_ptr(path));
+                                        assert_se(isempty(p));
+                                } else
+                                        assert_se(!p);
+                                assert_se(!e);
+                        }
+                        assert_se(r == ret);
+                        assert_se(strv_isempty(expected));
+                        return;
+                }
+
+                assert_se(e);
+                assert_se(strcspn(e, "/") == (size_t) r);
+                assert_se(strlen_ptr(*expected) == (size_t) r);
+                assert_se(strneq(e, *expected++, r));
+
+                assert_se(p);
+                log_debug("p=%s", p);
+                if (!isempty(*expected))
+                        assert_se(startswith(p, *expected));
+                else if (ret >= 0) {
+                        assert_se(p == path + strlen_ptr(path));
+                        assert_se(isempty(p));
+                }
+        }
+}
+
+static void test_path_find_first_component(void) {
+        _cleanup_free_ char *hoge = NULL;
+        char foo[NAME_MAX * 2];
+
+        test_path_find_first_component_one(NULL, false, NULL, 0);
+        test_path_find_first_component_one("", false, NULL, 0);
+        test_path_find_first_component_one("/", false, NULL, 0);
+        test_path_find_first_component_one(".", false, NULL, 0);
+        test_path_find_first_component_one("./", false, NULL, 0);
+        test_path_find_first_component_one("./.", false, NULL, 0);
+        test_path_find_first_component_one("..", false, NULL, -EINVAL);
+        test_path_find_first_component_one("/..", false, NULL, -EINVAL);
+        test_path_find_first_component_one("./..", false, NULL, -EINVAL);
+        test_path_find_first_component_one("////./././//.", false, NULL, 0);
+        test_path_find_first_component_one("a/b/c", false, STRV_MAKE("a", "b", "c"), 0);
+        test_path_find_first_component_one("././//.///aa/bbb//./ccc", false, STRV_MAKE("aa", "bbb", "ccc"), 0);
+        test_path_find_first_component_one("././//.///aa/.../../bbb//./ccc/.", false, STRV_MAKE("aa", "..."), -EINVAL);
+        test_path_find_first_component_one("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/.", false, STRV_MAKE("aaa", ".bbb"), -EINVAL);
+        test_path_find_first_component_one("a/foo./b//././/", false, STRV_MAKE("a", "foo.", "b"), 0);
+
+        test_path_find_first_component_one(NULL, true, NULL, 0);
+        test_path_find_first_component_one("", true, NULL, 0);
+        test_path_find_first_component_one("/", true, NULL, 0);
+        test_path_find_first_component_one(".", true, NULL, 0);
+        test_path_find_first_component_one("./", true, NULL, 0);
+        test_path_find_first_component_one("./.", true, NULL, 0);
+        test_path_find_first_component_one("..", true, STRV_MAKE(".."), 0);
+        test_path_find_first_component_one("/..", true, STRV_MAKE(".."), 0);
+        test_path_find_first_component_one("./..", true, STRV_MAKE(".."), 0);
+        test_path_find_first_component_one("////./././//.", true, NULL, 0);
+        test_path_find_first_component_one("a/b/c", true, STRV_MAKE("a", "b", "c"), 0);
+        test_path_find_first_component_one("././//.///aa/bbb//./ccc", true, STRV_MAKE("aa", "bbb", "ccc"), 0);
+        test_path_find_first_component_one("././//.///aa/.../../bbb//./ccc/.", true, STRV_MAKE("aa", "...", "..", "bbb", "ccc"), 0);
+        test_path_find_first_component_one("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/.", true, STRV_MAKE("aaa", ".bbb", "..", "c.", "d.dd", "..eeee"), 0);
+        test_path_find_first_component_one("a/foo./b//././/", true, STRV_MAKE("a", "foo.", "b"), 0);
+
+        memset(foo, 'a', sizeof(foo) -1);
+        char_array_0(foo);
+
+        test_path_find_first_component_one(foo, false, NULL, -EINVAL);
+        test_path_find_first_component_one(foo, true, NULL, -EINVAL);
+
+        hoge = strjoin("a/b/c/", foo, "//d/e/.//f/");
+        assert_se(hoge);
+
+        test_path_find_first_component_one(hoge, false, STRV_MAKE("a", "b", "c"), -EINVAL);
+        test_path_find_first_component_one(hoge, true, STRV_MAKE("a", "b", "c"), -EINVAL);
+}
+
 static void test_last_path_component(void) {
         assert_se(last_path_component(NULL) == NULL);
         assert_se(streq(last_path_component("a/b/c"), "c"));
@@ -718,6 +810,7 @@ int main(int argc, char **argv) {
         test_path_startswith();
         test_prefix_root();
         test_file_in_same_dir();
+        test_path_find_first_component();
         test_last_path_component();
         test_path_extract_filename();
         test_filename_is_valid();